package pass

import (
	"crypto/rand"
	"crypto/subtle"
	"fmt"
	"golang.org/x/crypto/blowfish"
	"io"
	"strconv"
)

var magicCipherData = []byte{
	0x4f, 0x72, 0x70, 0x68,
	0x65, 0x61, 0x6e, 0x42,
	0x65, 0x68, 0x6f, 0x6c,
	0x64, 0x65, 0x72, 0x53,
	0x63, 0x72, 0x79, 0x44,
	0x6f, 0x75, 0x62, 0x74,
}

const (
	Ver_2a          = "2a"
	Ver_2b          = "2b"
	MinCost         = 4
	MaxCost         = 31
	DefaultCost     = 12
	SALT_LEN        = 16
	encodedSaltSize = 22
	HASH_LEN        = 23
	encodedHashSize = 31
)

type BcryptPwd struct {
	version []byte
	cost    int
	salt    []byte
	hash    []byte
}

func NewBcryptPwd(ver string, cost int) PasswordEncoder {
	if cost < MinCost || cost > MaxCost {
		cost = DefaultCost
	}
	return &BcryptPwd{version: []byte(ver), cost: cost, salt: nil}
}

func (b *BcryptPwd) Encode(v string) (string, error) {
	err := b.onlyHash([]byte(v))
	if err != nil {
		return "", err
	}
	return b.toString(), nil
}

func (b *BcryptPwd) onlyHash(key []byte) error {
	if b.salt == nil {
		b.salt = make([]byte, SALT_LEN)
		r, _ := io.ReadFull(rand.Reader, b.salt)
		if r != SALT_LEN {
			return SaltFailed
		}
	}

	ckey := append(key[:len(key):len(key)], 0)

	c, err := blowfish.NewSaltedCipher(ckey, b.salt)
	if err != nil {
		return err
	}

	var i, rounds uint64
	rounds = 1 << b.cost
	for i = 0; i < rounds; i++ {
		blowfish.ExpandKey(ckey, c)
		blowfish.ExpandKey(b.salt, c)
	}

	b.hash = make([]byte, len(magicCipherData))
	copy(b.hash, magicCipherData)

	for i := 0; i < 24; i += 8 {
		for j := 0; j < 64; j++ {
			c.Encrypt(b.hash[i:i+8], b.hash[i:i+8])
		}
	}
	return nil
}

func (b *BcryptPwd) toString() string {
	arr := make([]byte, 60)
	arr[0] = '$'
	arr[1] = b.version[0]
	arr[2] = b.version[1]
	arr[3] = '$'
	n := 4
	copy(arr[n:], []byte(fmt.Sprintf("%02d", b.cost)))
	n += 2
	arr[n] = '$'
	n++
	copy(arr[n:], base64Encode(b.salt))            // base64 salt
	n += encodedSaltSize                           // base64 salt len = 22
	copy(arr[n:], base64Encode(b.hash[:HASH_LEN])) // 只取 HASH (0..22), 第 23 字符未使用
	n += encodedHashSize
	return string(arr[:n])
}

func (b *BcryptPwd) Matches(rawpass string, encpass string) bool {
	// 解析
	other, err := parseBcryptPasswordEncoder([]byte(encpass))
	if err != nil {
		return false
	}

	// 版本和cost不匹配，直接false
	if subtle.ConstantTimeCompare(b.version, other.version) != 1 || b.cost != other.cost {
		return false
	}

	// 用相同的salt
	b.salt = make([]byte, SALT_LEN)
	copy(b.salt, other.salt)
	_ = b.onlyHash([]byte(rawpass))

	// 只取 onlyHash (0..22), 第 23 字符未使用
	return subtle.ConstantTimeCompare(b.hash[0:HASH_LEN], other.hash) == 1
}

func parseBcryptPasswordEncoder(encpass []byte) (*BcryptPwd, error) {
	if len(encpass) < 59 {
		return nil, ParseFailed
	}
	if encpass[0] != '$' {
		return nil, ParseFailed
	}
	encpass = encpass[1:]

	version := encpass[0:2]
	encpass = encpass[2:]

	if encpass[0] != '$' {
		return nil, ParseFailed
	}
	encpass = encpass[1:]

	cost, _ := strconv.Atoi(string(encpass[0:2]))
	encpass = encpass[2:]

	if encpass[0] != '$' {
		return nil, ParseFailed
	}
	encpass = encpass[1:]

	salt, _ := base64Decode(encpass[:encodedSaltSize])
	hash, _ := base64Decode(encpass[encodedSaltSize:])

	return &BcryptPwd{version: version, cost: cost, salt: salt, hash: hash}, nil
}
